import {
  ChangeDetectionStrategy, Component,
  ElementRef, OnInit, Input, Output,
  ViewChild, ViewEncapsulation, Inject, ChangeDetectorRef,
  OnDestroy, forwardRef, EventEmitter
} from '@angular/core';
import {DOCUMENT} from '@angular/common';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';

import {fromEvent, Observable, merge, Subscription} from 'rxjs';
import {distinctUntilChanged, filter, map, pluck, takeUntil, tap} from 'rxjs/operators';

import {SliderEventObserverConfig, SliderValue} from '../../../../../data-types/slider.types';
import {sliderEvent, isInArray, getElementOffset, limitNumberInRange, getPercent} from '../../../../../helpers/slider';


@Component({
  selector: 'app-slider',
  templateUrl: './slider.component.html',
  styleUrls: ['./slider.component.less'],
  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush,
  providers: [
    {
      // 自定义表单控件
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => SliderComponent),
      multi: true
    }
  ]
})
export class SliderComponent implements OnInit, OnDestroy, ControlValueAccessor {

  @Input() vertical: boolean;

  @Input() sliderMin: number;

  @Input() sliderMax: number;

  @Input() bufferOffset: SliderValue;

  @Output() valueAfterChange = new EventEmitter<number>();

  // 最外层的 slider 容器
  @ViewChild('sliderRef', {static: true}) private sliderRef: ElementRef;

  private sliderDom: HTMLDivElement;

  // 表示是否正在滑动
  public isDragging: boolean;

  public value: SliderValue;

  public offset: SliderValue;

  // 绑定流
  private dragStart$: Observable<number>;

  private dragMove$: Observable<number>;

  private dragEnd$: Observable<Event>;

  // 取消订阅
  private dragStartUnsubscribe$: Subscription | null;

  private dragMoveUnsubscribe$: Subscription | null;

  private dragEndUnsubscribe$: Subscription | null;

  constructor(
    @Inject(DOCUMENT) private doc: Document,
    private cdr: ChangeDetectorRef
  ) {
    this.vertical = false;

    this.sliderMin = 0;

    this.sliderMax = 100;

    this.isDragging = false;

    this.value = null;

    this.offset = null;
  }

  private createDraggingObservables() {
    const orientField = this.vertical ? 'pageY' : 'pageX';

    const mouse: SliderEventObserverConfig = {
      start: 'mousedown',
      move: 'mousemove',
      end: 'mouseup',
      filter: (e: MouseEvent) => e instanceof MouseEvent,
      pluckKey: [orientField]
    };

    const touch: SliderEventObserverConfig = {
      start: 'touchstart',
      move: 'touchmove',
      end: 'touchend',
      filter: (e: MouseEvent) => e instanceof TouchEvent,
      pluckKey: ['touches', '0', orientField]
    };

    [mouse, touch].forEach(source => {
      const {start, move, end, filter: eventFilter, pluckKey} = source;
      source.startPlucked$ = fromEvent(this.sliderDom, start)
        .pipe(
          filter(eventFilter),
          tap(sliderEvent),
          pluck(...pluckKey),
          map((position: number) => this.findClosestValue(position))
        );

      source.end$ = fromEvent(this.doc, end);

      source.moveResolved$ = fromEvent(this.doc, move)
        .pipe(
          filter(eventFilter),
          tap(sliderEvent),
          pluck(...pluckKey),
          distinctUntilChanged(),
          map((position: number) => this.findClosestValue(position)),
          takeUntil(source.end$)
        );

    });

    this.dragStart$ = merge(mouse.startPlucked$, touch.startPlucked$);
    this.dragMove$ = merge(mouse.moveResolved$, touch.moveResolved$);
    this.dragEnd$ = merge(mouse.end$, touch.end$);
  }

  // 订阅
  private subscribeDrag(events: Array<string> = ['start', 'move', 'end']) {
    if (isInArray(events, 'start') && this.dragStart$ && !this.dragStartUnsubscribe$) {
      this.dragStartUnsubscribe$ = this.dragStart$.subscribe(this.onDragStart.bind(this));
    }

    if (isInArray(events, 'move') && this.dragMove$ && !this.dragMoveUnsubscribe$) {
      this.dragMoveUnsubscribe$ = this.dragMove$.subscribe(this.onDragMove.bind(this));
    }

    if (isInArray(events, 'end') && this.dragEnd$ && !this.dragEndUnsubscribe$) {
      this.dragEndUnsubscribe$ = this.dragEnd$.subscribe(this.onDragEnd.bind(this));
    }
  }

  // 取消订阅
  private unsubscribeDrag(events: Array<string> = ['start', 'move', 'end']) {
    if (isInArray(events, 'start') && this.dragStartUnsubscribe$) {
      this.dragStartUnsubscribe$.unsubscribe();
      this.dragStartUnsubscribe$ = null;
    }

    if (isInArray(events, 'move') && this.dragMoveUnsubscribe$) {
      this.dragMoveUnsubscribe$.unsubscribe();
      this.dragMoveUnsubscribe$ = null;
    }

    if (isInArray(events, 'end') && this.dragEndUnsubscribe$) {
      this.dragEndUnsubscribe$.unsubscribe();
      this.dragEndUnsubscribe$ = null;
    }
  }

  private onDragStart(value: number) {
    this.toggleDragMoving(true);
    this.setValue(value);
  }

  private onDragMove(value: number) {
    if (this.isDragging) {
      this.setValue(value);
      // 手动进行变更检测
      this.cdr.markForCheck();
    }
  }

  private onDragEnd() {
    this.valueAfterChange.emit(this.value);
    this.toggleDragMoving(false);
    this.cdr.markForCheck();
  }

  private setValue(value: SliderValue, needCheck = false) {
    if (needCheck) {
      if (this.isDragging) {
        return;
      }
      this.value = this.formatValue(value);
      this.updateTrackAndHandles();
    } else if (!this.valueEqual(this.value, value)) {
      this.value = value;
      this.updateTrackAndHandles();
      this.onValueChange(this.value);
    }
  }

  private onValueChange(value: SliderValue): void {

  }

  private onValueTouched(): void {

  }

  private formatValue(value: SliderValue): SliderValue {
    let res = value;
    if (this.assertValueValid(value)) {
      res = this.sliderMin;
    } else {
      res = limitNumberInRange(value, this.sliderMin, this.sliderMax);
    }
    return res;
  }

  private assertValueValid(value: SliderValue): boolean {
    return isNaN(typeof value !== 'number' ? parseFloat(value) : value);
  }

  private valueEqual(currentValue: SliderValue, newValue: SliderValue): boolean {
    if (typeof currentValue !== typeof newValue) {
      return false;
    }
    return currentValue === newValue;
  }

  private updateTrackAndHandles() {
    this.offset = this.getValueToOffset(this.value);
    this.cdr.markForCheck();
  }

  private getValueToOffset(value: SliderValue): SliderValue {
    return getPercent(value, this.sliderMin, this.sliderMax);
  }

  private toggleDragMoving(movable: boolean) {
    this.isDragging = movable;

    // 如果正在移动
    if (movable) {
      this.subscribeDrag(['move', 'end']);
    } else {
      this.unsubscribeDrag(['move', 'end']);
    }
  }

  private getSliderLength(): number {
    // 区分为垂直和水平方向
    return this.vertical ? this.sliderDom.clientHeight : this.sliderDom.clientWidth;
  }

  private getSliderStartPosition(): number {
    const offset = getElementOffset(this.sliderDom);
    return this.vertical ? offset.top : offset.left;
  }

  private findClosestValue(position: number): number {
    // 公式： position - 滑块组件总长 === (value - min) / (max - min);

    // 获取滑块总长度
    const sliderLength = this.getSliderLength();

    // 获取滑块左端点 / 上端点
    const sliderStart = this.getSliderStartPosition();

    // 滑块当前位置和总长比例
    const ratio = limitNumberInRange((position - sliderStart) / sliderLength, 0, 1);

    // 真实的比例
    const realRatio = this.vertical ? 1 - ratio : ratio;

    // 得到最终的位置
    return realRatio * (this.sliderMax - this.sliderMin) + this.sliderMin;
  }

  ngOnInit(): void {
    this.sliderDom = this.sliderRef.nativeElement;
    this.createDraggingObservables();
    this.subscribeDrag(['start']);
  }

  ngOnDestroy(): void {
    this.unsubscribeDrag();
  }

  // 自定义表单控件，实现接口 ControlValueAccessor
  // 读取值并且赋值
  writeValue(value: SliderValue): void {
    this.setValue(value, true);
  }

  // 发射 change 事件
  registerOnChange(fn: (value: SliderValue) => void): void {
    this.onValueChange = fn;
  }

  // 发射 touch 事件
  registerOnTouched(fn: () => void) {
    this.onValueTouched = fn;
  }


}
